#include "FanObj.h"

#include <string.h>
#include <fan-llvm/core/Core.h>
#include "ArgErr.h"
#include "CastErr.h"
#include "Env.h"
#include "Field.h"
#include "Func.h"
#include "List.h"
#include "Method.h"
#include "NotImmutableErr.h"
#include "OutStream.h"
#include "Slot.h"
#include "Str.h"
#include "Type.h"
#include "UnsupportedErr.h"


Type* Obj_classType = NULL;
VTable* Obj_classVTable = NULL;


/**********************************************
 *
 * Methods and fields
 *
 **********************************************/


/* Void make() */
void Obj_make(Obj self)
{
}


/* Bool equals(Obj? that) */
Bool Obj_equals(Obj self, NullableObj that)
{
  return (VAL_GET_REF(self) == VAL_GET_REF(that));
}


/* Int compare(Obj that) */
Int Obj_compare(Obj self, Obj that)
{
  Str* selfStr = OBJ_VTABLE(self)->toStr(self);
  Str* thatStr = OBJ_VTABLE(self)->toStr(that);
  return Str_compare(VAL_MAKE_REF(selfStr), VAL_MAKE_REF(thatStr));
}


/* Int hash() */
Int Obj_hash(Obj self)
{
  return (intptr_t)VAL_GET_REF(self);
}


/* Str toStr() */
Str* Obj_toStr(Obj self)
{
  return Str_newWithFormat("%r %i", Obj_typeof(self), (Int)(intptr_t)VAL_GET_REF(self));
}


/* Obj? trap(Str name, Obj?[]? args) */
NullableObj Obj_trap_1(Obj self, Str* name)
{
  return OBJ_VTABLE(self)->trap(self, name, NULL);
}


/* Obj? trap(Str name, Obj?[]? args) */
NullableObj Obj_trap(Obj self, Str* name, List* args)
{
  Type* type = Obj_typeof(self);
  Slot* slot = Type_slot(type, name, TRUE);

  if (Obj_typeof(VAL_MAKE_REF(slot)), Method_classType)
  {
    Method* method = (Method*)slot;
    return Method_callOn(method, self, args);
  }
  else
  {
    Field* field = (Field*)slot;
    Int argSize = (args != NULL) ? List_size(args) : 0;

    if (argSize == 0)
      return Field_get(field, self);
    else if (argSize == 1)
    {
      NullableObj val = List_get(args, 0);
      Field_set(field, self, val);
      return val;
    }
    else
    {
      exceptionThrow(ArgErr_newWithFormattedMsg("Invalid number of args to get or set field '%r'", name));
      return VAL_NULL;
    }
  }
}


/* This with(|This->Void| f) */
Obj Obj_with(Obj self, Func* f)
{
  Func_call_1(f, self);
  return self;
}


/* Bool isImmutable() */
Bool Obj_isImmutable(Obj self)
{
  if (OBJ_VTABLE(self)->isImmutable != NULL)
    return OBJ_VTABLE(self)->isImmutable(self);
  else
    return Type_isConst(Obj_typeof(self));
}


/* This toImmutable() */
Obj Obj_toImmutable(Obj self)
{
  if (OBJ_VTABLE(self)->toImmutable != NULL)
    return OBJ_VTABLE(self)->toImmutable(self);
  else
  {
    if (!Type_isConst(Obj_typeof(self)))
      exceptionThrow(NotImmutableErr_newWithFormattedMsg("%r", Obj_typeof(self)));
    return self;
  }
}


/* Type typeof() */
Type* Obj_typeof(Obj self)
{
  return VAL_GET_VTABLE(self)->instanceType;
}


/* Void echo(Obj? x) */
void Obj_echo_0(void)
{
  Obj_echo(VAL_MAKE_REF(Str_defVal()));
}


/* Void echo(Obj? x) */
void Obj_echo(NullableObj x)
{
  OutStream_printLine(Env_out(Env_cur()), x);
}


/**********************************************
 *
 * Extra methods
 *
 **********************************************/


/* Allocate a zero-initialized instance of size 'instanceSize' and with the given VTable */
FanObj* Obj_allocate(size_t instanceSize, VTable* vtable)
{
  FanObj* obj = memoryAlloc(instanceSize);
  obj->vtable = vtable;
  return obj;
}


/* Cast this instance into the given class-type.
 * If the types mismatch and 'checked' is false, return NULL, otherwise throw CastErr */
void* Obj_castToClass(NullableObj self, Type* type, Bool checked)
{
  if (VAL_IS_NULL(self))
    return NULL;

  Type* instanceType = VAL_GET_VTABLE(self)->instanceType;
  if (instanceType->depth >= type->depth && instanceType->hierarchy[type->depth] == type)
    return VAL_GET_REF(self);
  else
  {
    if (checked)
      exceptionThrow(CastErr_newWithTypes(instanceType, type));
    return NULL;
  }
}


/* Cast this instance into the given mixin-type.
 * If the types mismatch and 'checked' is false, return NULL, otherwise throw CastErr */
void* Obj_castToMixin(NullableObj self, Type* type, Bool checked)
{
  exceptionThrow(UnsupportedErr_newWithFormattedMsg("sys::Obj.castToMixin not implemented yet"));
  return NULL;
}


/**********************************************
 *
 * Type-system functions
 *
 **********************************************/


Type* Obj_initType(void)
{
  if (Obj_classType == NULL)
  {
    Type* baseType = NULL;
    VTable* vtable = Obj_initVTable();
    Type* type = Type_new("Obj", baseType, vtable, sizeof(FanObj), sizeof(FanObjVTable));
    Type_registerCtorImplementation(type, "make", Obj_make, 0);
    Type_registerMethodImplementation(type, "compare", Obj_compare, 1);
    Type_registerMethodImplementation(type, "echo", Obj_echo_0, 0);
    Type_registerMethodImplementation(type, "echo", Obj_echo, 1);
    Type_registerMethodImplementation(type, "equals", Obj_equals, 1);
    Type_registerMethodImplementation(type, "hash", Obj_hash, 0);
    Type_registerMethodImplementation(type, "isImmutable", Obj_isImmutable, 0);
    Type_registerMethodImplementation(type, "toImmutable", Obj_toImmutable, 0);
    Type_registerMethodImplementation(type, "toStr", Obj_toStr, 0);
    Type_registerMethodImplementation(type, "trap", Obj_trap_1, 1);
    Type_registerMethodImplementation(type, "trap", Obj_trap, 2);
    Type_registerMethodImplementation(type, "typeof", Obj_typeof, 0);
    Type_registerMethodImplementation(type, "with", Obj_with, 1);
    Obj_classType = type;
    Obj_initStatic();
  }
  return Obj_classType;
}


VTable* Obj_initVTable(void)
{
  if (Obj_classVTable == NULL)
  {
    VTable* vtable = memoryAlloc(sizeof(FanObjVTable));
    ((FanObjVTable*)vtable)->compare = Obj_compare;
    ((FanObjVTable*)vtable)->equals = Obj_equals;
    ((FanObjVTable*)vtable)->hash = Obj_hash;
    ((FanObjVTable*)vtable)->toStr = Obj_toStr;
    ((FanObjVTable*)vtable)->trap_1 = Obj_trap_1;
    ((FanObjVTable*)vtable)->trap = Obj_trap;
    ((FanObjVTable*)vtable)->with = Obj_with;
    ((FanObjVTable*)vtable)->isImmutable = NULL;
    ((FanObjVTable*)vtable)->toImmutable = NULL;
    Obj_classVTable = vtable;
  }
  return Obj_classVTable;
}


void Obj_initStatic(void)
{
}
